/*
一个小型的Web服务器,目前支持的功能有限
只支持GET方法
 */
#include "myf.h"

void doit(int fd);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char *filename, char *cgiargs);
void serve_static(int fd, char *filename, int filesize);
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *filename, char *cgiargs);
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg);

int main(int argc, char **argv)
{
    int listenfd, connfd;
    char hostname[MAXLINE], port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;

    // 检查命令行参数,需要在启动服务器的时候指定默认的端口号,HTTP默认的端口号为80,可以考虑使用
    // 使用默认80端口时需要注意,linux安全限制普通用户只能使用大于1024的端口,你可以使用root运行尝试
    if (argc != 2 || atoi(argv[1]) < 0 || atoi(argv[1]) > 65535)
    { //如果没有指定合适的端口
        //fprintf(stderr, "usage: %s <port>\n", argv[0]);
        //exit(1);
        listenfd = Open_listenfd("80"); //使用默认的80端口监听,这个版本暂时这样实现,还可以通过探测的方式获取可用的最小端口
    }
    else
    {
        listenfd = Open_listenfd(argv[1]); //使用指定的端口监听
    }

    while (1)
    {
        clientlen = sizeof(clientaddr);
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);//获取连接套接字描述符,用于和客户端通信
        Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);//获取客户端信息
        printf("接受请求的来源为 (%s, %s)\n", hostname, port);
        doit(connfd);//开始通信,
        Close(connfd);//关闭资源
    }
}

void doit(int fd)
{
    int is_static;
    struct stat sbuf;
    char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    char filename[MAXLINE], cgiargs[MAXLINE];
    rio_t rio;

    //读取请求行和请求头
    Rio_readinitb(&rio, fd);
    if (!Rio_readlineb(&rio, buf, MAXLINE)) //读取请求行
        return;
    printf("%s", buf);
    sscanf(buf, "%s %s %s", method, uri, version); //解析请求头,具体参考HTTP协议
    if (strcasecmp(method, "GET"))
    {//如果请求方法不是GET方式,这个版本中先实现GET,下一版本中添加post方法支持
        clienterror(fd, method, "501", "无实现", "没有实现该方法");
        return;
    }
    read_requesthdrs(&rio);

    is_static = parse_uri(uri, filename, cgiargs);//判断是不是访问静态内容
    if (stat(filename, &sbuf) < 0)
    {//如果差找不到对应资源
        clienterror(fd, filename, "404", "未发现资源", "没有发现相应的资源");
        return;
    }

    if (is_static)
    { /* 静态内容处理 */
        if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode))
        {//如果不是普通文件或者用户没有读取权限
            clienterror(fd, filename, "403", "访问被禁止", "您没有权限读取该文件");
            return;
        }
        serve_static(fd, filename, sbuf.st_size);//通过静态方式处理
    }
    else
    { /* 动态内容处理 */
        if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode))
        {//如果不是普通文件或者用户没有执行权限
            clienterror(fd, filename, "403", "访问被禁止", "您没有权限运行该CGI程序");
            return;
        }
        serve_dynamic(fd, filename, cgiargs);//通过动态方式处理
    }
}

void read_requesthdrs(rio_t *rp)
{
    char buf[MAXLINE];

    Rio_readlineb(rp, buf, MAXLINE);
    printf("%s", buf);
    while (strcmp(buf, "\r\n"))
    {
        Rio_readlineb(rp, buf, MAXLINE);
        printf("%s", buf);
    }
    return;
}

int parse_uri(char *uri, char *filename, char *cgiargs)
{
    char *ptr;

    if (!strstr(uri, "cgi-bin"))
    { /* 静态内容 */
        strcpy(cgiargs, "");
        strcpy(filename, ".");
        strcat(filename, uri);
        if (uri[strlen(uri) - 1] == '/')
        {
            //建议的方式,通过递归遍历的方式搜索default 或 index为索引页,可以留在下一个版本实现
            strcat(filename, "wwwroot/default.html");
        }
        return 1;
    }
    else
    { /* 动态内容 */
        ptr = index(uri, '?');
        if (ptr)
        {
            strcpy(cgiargs, ptr + 1);
            *ptr = '\0';
        }
        else
            strcpy(cgiargs, "");
        strcpy(filename, ".");
        strcat(filename, uri);
        return 0;
    }
}

//响应静态资源请求
void serve_static(int fd, char *filename, int filesize)
{
    int srcfd;
    char *srcp, filetype[MAXLINE], buf[MAXBUF];

    //发送HTTP响应头给客户端,响应的MIME类型可以在线查询
    get_filetype(filename, filetype);
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Server: wzf Web Server\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-length: %d\r\n", filesize);
    Rio_writen(fd, buf, strlen(buf));
    //sprintf(buf, "Content-type: %s\r\n\r\n", filetype);
    //Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-type: text/html;charset=UTF-8\r\n\r\n"); //要注意,HTTP头结尾的标识是一个空行,所以不要忘记喽
    Rio_writen(fd, buf, strlen(buf));

    //发送响应体给客户端
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    Close(srcfd);
    Rio_writen(fd, srcp, filesize);
    Munmap(srcp, filesize);
}

//获取文件类型,常见的文件类型可以考虑都添加上
void get_filetype(char *filename, char *filetype)
{
    if (strstr(filename, ".html"))
        strcpy(filetype, "text/html");
    else if (strstr(filename, ".gif"))
        strcpy(filetype, "image/gif");
    else if (strstr(filename, ".png"))
        strcpy(filetype, "image/png");
    else if (strstr(filename, ".jpg"))
        strcpy(filetype, "image/jpeg");
    else if (strstr(filename, ".json"))
        strcpy(filetype, "application/json");
    else
        strcpy(filetype, "text/plain"); //其他情况下都认为是简单文本
}

//响应动态资源请求
void serve_dynamic(int fd, char *filename, char *cgiargs)
{
    char buf[MAXLINE], *emptylist[] = {NULL};

    /* 返回响应头 */
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Server: wzf Web Server\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-type: text/html;charset=UTF-8"); //解决返回中文乱码的问题
    Rio_writen(fd, buf, strlen(buf));

    if (Fork() == 0)
    {//派生的子进程负责和用户通信
        setenv("QUERY_STRING", cgiargs, 1);//首先将查询字串设置为环境变量,方便其他cgi进程获取
        Dup2(fd, STDOUT_FILENO);             //重定向标准输出到客户端的fd
        Execve(filename, emptylist, environ); /* 运行CGI程序 */
    }
    Wait(NULL);//也可以考虑使用waitpid(0,NULL,0)
}

//给客户端返回错误的访问信息
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg)
{
    char buf[MAXLINE];

    //响应头信息
    sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
    Rio_writen(fd, buf, strlen(buf));
    // sprintf(buf, "Content-type: text/html\r\n\r\n");
    // Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Content-type: text/html;charset=UTF-8\r\n\r\n"); //要注意,HTTP头结尾的标识是一个空行,所以不要忘记喽
    Rio_writen(fd, buf, strlen(buf));

    //响应体信息
    sprintf(buf, "<html><title>错误页面</title>");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "<body>\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "%s: %s\r\n", errnum, shortmsg);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "<p>%s: %s\r\n", longmsg, cause);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "<hr><em>The wzf Web server [By wzf]</em>\r\n");
    Rio_writen(fd, buf, strlen(buf));
}
